import numpy as np
import pandas as pd
!pip install openpyxl
!pip install geopandas
!pip install matplotlib
Requirement already satisfied: openpyxl in c:\users\evahr\anaconda3\envs\master\lib\site-packages (3.1.5) Requirement already satisfied: et-xmlfile in c:\users\evahr\anaconda3\envs\master\lib\site-packages (from openpyxl) (2.0.0) Requirement already satisfied: geopandas in c:\users\evahr\anaconda3\envs\master\lib\site-packages (1.1.1) Requirement already satisfied: numpy>=1.24 in c:\users\evahr\anaconda3\envs\master\lib\site-packages (from geopandas) (2.3.2) Requirement already satisfied: pyogrio>=0.7.2 in c:\users\evahr\anaconda3\envs\master\lib\site-packages (from geopandas) (0.11.0) Requirement already satisfied: packaging in c:\users\evahr\anaconda3\envs\master\lib\site-packages (from geopandas) (25.0) Requirement already satisfied: pandas>=2.0.0 in c:\users\evahr\anaconda3\envs\master\lib\site-packages (from geopandas) (2.3.1) Requirement already satisfied: pyproj>=3.5.0 in c:\users\evahr\anaconda3\envs\master\lib\site-packages (from geopandas) (3.7.1) Requirement already satisfied: shapely>=2.0.0 in c:\users\evahr\anaconda3\envs\master\lib\site-packages (from geopandas) (2.1.1) Requirement already satisfied: python-dateutil>=2.8.2 in c:\users\evahr\anaconda3\envs\master\lib\site-packages (from pandas>=2.0.0->geopandas) (2.9.0.post0) Requirement already satisfied: pytz>=2020.1 in c:\users\evahr\anaconda3\envs\master\lib\site-packages (from pandas>=2.0.0->geopandas) (2025.2) Requirement already satisfied: tzdata>=2022.7 in c:\users\evahr\anaconda3\envs\master\lib\site-packages (from pandas>=2.0.0->geopandas) (2025.2) Requirement already satisfied: certifi in c:\users\evahr\anaconda3\envs\master\lib\site-packages (from pyogrio>=0.7.2->geopandas) (2025.7.14) Requirement already satisfied: six>=1.5 in c:\users\evahr\anaconda3\envs\master\lib\site-packages (from python-dateutil>=2.8.2->pandas>=2.0.0->geopandas) (1.17.0) Requirement already satisfied: matplotlib in c:\users\evahr\anaconda3\envs\master\lib\site-packages (3.10.5) Requirement already satisfied: contourpy>=1.0.1 in c:\users\evahr\anaconda3\envs\master\lib\site-packages (from matplotlib) (1.3.3) Requirement already satisfied: cycler>=0.10 in c:\users\evahr\anaconda3\envs\master\lib\site-packages (from matplotlib) (0.12.1) Requirement already satisfied: fonttools>=4.22.0 in c:\users\evahr\anaconda3\envs\master\lib\site-packages (from matplotlib) (4.59.0) Requirement already satisfied: kiwisolver>=1.3.1 in c:\users\evahr\anaconda3\envs\master\lib\site-packages (from matplotlib) (1.4.8) Requirement already satisfied: numpy>=1.23 in c:\users\evahr\anaconda3\envs\master\lib\site-packages (from matplotlib) (2.3.2) Requirement already satisfied: packaging>=20.0 in c:\users\evahr\anaconda3\envs\master\lib\site-packages (from matplotlib) (25.0) Requirement already satisfied: pillow>=8 in c:\users\evahr\anaconda3\envs\master\lib\site-packages (from matplotlib) (11.3.0) Requirement already satisfied: pyparsing>=2.3.1 in c:\users\evahr\anaconda3\envs\master\lib\site-packages (from matplotlib) (3.2.3) Requirement already satisfied: python-dateutil>=2.7 in c:\users\evahr\anaconda3\envs\master\lib\site-packages (from matplotlib) (2.9.0.post0) Requirement already satisfied: six>=1.5 in c:\users\evahr\anaconda3\envs\master\lib\site-packages (from python-dateutil>=2.7->matplotlib) (1.17.0)
1. Hist de precios de venta Madrid¶
evolucion_precio = pd.ExcelFile(r"C:\Users\evahr\Downloads\TFM-idealista\Hist de precios de venta Madrid.xlsx")
print(evolucion_precio.sheet_names)
['Arganzuela', 'Barajas', 'Carabanchel', 'Centro', 'Chamartín', 'Chamberí', 'Ciudad Lineal', 'Fuencarral-El Pardo', 'Hortaleza', 'Latina', 'Moncloa-Aravaca', 'Moratalaz', 'Puente de Vallecas', 'Retiro', 'Salamanca', 'San Blas-Canillejas', 'Tetuán', 'Usera', 'Vicálvaro', 'Villa de Vallecas', 'Villaverde']
# Leer cada hoja en un diccionario {nombre_distrito: DataFrame}
district_data = {
sheet_name.strip(): evolucion_precio.parse(sheet_name)
for sheet_name in evolucion_precio.sheet_names
}
# Crear diferentes Data Frame por cada distrito
# Crear diferentes DataFrame por cada distrito
df_arganzuela = evolucion_precio.parse("Arganzuela")
df_barajas = evolucion_precio.parse("Barajas")
df_carabanchel = evolucion_precio.parse("Carabanchel")
df_centro = evolucion_precio.parse("Centro")
df_chamartin = evolucion_precio.parse("Chamartín")
df_chamberi = evolucion_precio.parse("Chamberí")
df_ciudad_lineal = evolucion_precio.parse("Ciudad Lineal")
df_fuencarral = evolucion_precio.parse("Fuencarral-El Pardo")
df_hortaleza = evolucion_precio.parse("Hortaleza")
df_latina = evolucion_precio.parse("Latina")
df_moncloa = evolucion_precio.parse("Moncloa-Aravaca")
df_moratalaz = evolucion_precio.parse("Moratalaz")
df_puente_de_vallecas = evolucion_precio.parse("Puente de Vallecas")
df_retiro = evolucion_precio.parse("Retiro")
df_salamanca = evolucion_precio.parse("Salamanca")
df_san_blas = evolucion_precio.parse("San Blas-Canillejas")
df_tetuan = evolucion_precio.parse("Tetuán")
df_usera = evolucion_precio.parse("Usera")
df_vicalvaro = evolucion_precio.parse("Vicálvaro")
df_villa_de_vallecas = evolucion_precio.parse("Villa de Vallecas")
df_villaverde = evolucion_precio.parse("Villaverde")
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.cm as cm
import numpy as np
# Diccionario de dataframes por distrito
distritos = {
"Arganzuela": df_arganzuela,
"Barajas": df_barajas,
"Carabanchel": df_carabanchel,
"Centro": df_centro,
"Chamartín": df_chamartin,
"Chamberí": df_chamberi,
"Ciudad Lineal": df_ciudad_lineal,
"Fuencarral": df_fuencarral,
"Hortaleza": df_hortaleza,
"Latina": df_latina,
"Moncloa": df_moncloa,
"Moratalaz": df_moratalaz,
"Puente de Vallecas": df_puente_de_vallecas,
"Retiro": df_retiro,
"Salamanca": df_salamanca,
"San Blas": df_san_blas,
"Tetuán": df_tetuan,
"Usera": df_usera,
"Vicálvaro": df_vicalvaro,
"Villa de Vallecas": df_villa_de_vallecas,
"Villaverde": df_villaverde
}
# Eliminar el símbolo €/m2, limpia puntos de miles y comas decimales
# Lista de variables de tus DataFrames
nombres_distritos = [
'df_arganzuela', 'df_barajas', 'df_carabanchel', 'df_centro', 'df_chamartin',
'df_chamberi', 'df_ciudad_lineal', 'df_fuencarral', 'df_hortaleza', 'df_latina',
'df_moncloa', 'df_moratalaz', 'df_puente_de_vallecas', 'df_retiro', 'df_salamanca',
'df_san_blas', 'df_tetuan', 'df_usera', 'df_vicalvaro', 'df_villa_de_vallecas', 'df_villaverde'
]
# Bucle para limpiar y convertir la columna 'Precio m2' en todos
for nombre in nombres_distritos:
df = globals()[nombre]
# Asegurarse de que la columna sea tipo texto
df['Precio m2'] = df['Precio m2'].astype(str)
# Limpiar símbolos y formato
df['Precio m2'] = (
df['Precio m2']
.str.replace('€/m2', '', regex=False)
.str.replace('.', '', regex=False)
.str.replace(',', '.', regex=False)
)
# Convertir a float, 'nd' y errores serán NaN
df['Precio m2'] = pd.to_numeric(df['Precio m2'], errors='coerce')
# Eliminar filas con NaN en 'Precio m2'
df.dropna(subset=['Precio m2'], inplace=True)
# Lista de variables de tus DataFrames
nombres_distritos = [
'df_arganzuela', 'df_barajas', 'df_carabanchel', 'df_centro', 'df_chamartin',
'df_chamberi', 'df_ciudad_lineal', 'df_fuencarral', 'df_hortaleza', 'df_latina',
'df_moncloa', 'df_moratalaz', 'df_puente_de_vallecas', 'df_retiro', 'df_salamanca',
'df_san_blas', 'df_tetuan', 'df_usera', 'df_vicalvaro', 'df_villa_de_vallecas', 'df_villaverde'
]
import pandas as pd
# Crear lista de tuplas con el nombre del distrito y su DataFrame ya existente
distritos = [
("Arganzuela", df_arganzuela),
("Barajas", df_barajas),
("Carabanchel", df_carabanchel),
("Centro", df_centro),
("Chamartín", df_chamartin),
("Chamberí", df_chamberi),
("Ciudad Lineal", df_ciudad_lineal),
("Fuencarral", df_fuencarral),
("Hortaleza", df_hortaleza),
("Latina", df_latina),
("Moncloa", df_moncloa),
("Moratalaz", df_moratalaz),
("Puente de Vallecas", df_puente_de_vallecas),
("Retiro", df_retiro),
("Salamanca", df_salamanca),
("San Blas", df_san_blas),
("Tetuán", df_tetuan),
("Usera", df_usera),
("Vicálvaro", df_vicalvaro),
("Villa de Vallecas", df_villa_de_vallecas),
("Villaverde", df_villaverde)
]
# Añadir columna "Distrito" y asegurar que "Mes" sea columna, no índice
dataframes = []
for nombre, df in distritos:
df_mod = df.copy().reset_index()
df_mod["Distrito"] = nombre
dataframes.append(df_mod)
# Unir todos
hist_precios_madrid = pd.concat(dataframes, ignore_index=True)
hist_precios_madrid = hist_precios_madrid.drop(columns=['index'])
hist_precios_madrid.head()
| Mes | Precio m2 | Variación mensual | Variación trimestral | Variación anual | Distrito | |
|---|---|---|---|---|---|---|
| 0 | 2025-06-01 | 5729.0 | 0.01 | 0.073 | 0.181 | Arganzuela |
| 1 | 2025-05-01 | 5669.0 | 0.036 | 0.068 | 0.192 | Arganzuela |
| 2 | 2025-04-01 | 5472.0 | 0.024 | 0.043 | 0.182 | Arganzuela |
| 3 | 2025-03-01 | 5341.0 | 0.006 | 0.036 | 0.163 | Arganzuela |
| 4 | 2025-02-01 | 5310.0 | 0.012 | 0.049 | 0.162 | Arganzuela |
distritos_lista = list(hist_precios_madrid["Distrito"].unique())
print(distritos_lista)
['Arganzuela', 'Barajas', 'Carabanchel', 'Centro', 'Chamartín', 'Chamberí', 'Ciudad Lineal', 'Fuencarral', 'Hortaleza', 'Latina', 'Moncloa', 'Moratalaz', 'Puente de Vallecas', 'Retiro', 'Salamanca', 'San Blas', 'Tetuán', 'Usera', 'Vicálvaro', 'Villa de Vallecas', 'Villaverde']
# Convertimos la columna Precio m2 a numérica por si aún tiene texto
hist_precios_madrid["Precio m2"] = pd.to_numeric(
hist_precios_madrid["Precio m2"], errors="coerce"
)
# Sacar describe por distrito
describe_por_distrito = hist_precios_madrid.groupby("Distrito")["Precio m2"].describe()
print(describe_por_distrito)
count mean std min 25% 50% \
Distrito
Arganzuela 212.0 3656.174528 693.991459 2658.0 3009.25 3664.5
Barajas 206.0 3101.271845 384.311705 2546.0 2704.75 3163.0
Carabanchel 218.0 2243.949541 441.483739 1601.0 1853.00 2177.0
Centro 217.0 4426.686636 902.037716 3248.0 3731.00 4294.0
Chamartín 213.0 4804.633803 737.875135 3864.0 4223.00 4688.0
Chamberí 210.0 4855.171429 996.517313 3562.0 3994.25 4704.5
Ciudad Lineal 216.0 3006.333333 442.844621 2393.0 2556.75 3012.0
Fuencarral 215.0 3352.706977 437.548152 2713.0 2968.50 3369.0
Hortaleza 213.0 3487.281690 483.150476 2739.0 3012.00 3535.0
Latina 212.0 2284.872642 437.157219 1608.0 1877.50 2288.5
Moncloa 215.0 3785.823256 580.869560 2991.0 3303.00 3843.0
Moratalaz 190.0 2425.563158 411.901891 1873.0 2008.25 2494.5
Puente de Vallecas 216.0 1975.953704 464.945056 1293.0 1523.25 1922.5
Retiro 208.0 4209.115385 869.612727 3085.0 3439.25 4038.5
Salamanca 212.0 5314.169811 1249.417369 3991.0 4353.50 4845.0
San Blas 215.0 2624.693023 392.717014 2040.0 2231.50 2572.0
Tetuán 218.0 3447.266055 614.993667 2573.0 2912.50 3518.0
Usera 213.0 2077.244131 428.495389 1456.0 1701.00 2043.0
Vicálvaro 185.0 2269.535135 471.438571 1625.0 1828.00 2320.0
Villa de Vallecas 206.0 2399.320388 329.946802 1833.0 2099.00 2414.5
Villaverde 208.0 1812.322115 376.599067 1250.0 1485.50 1746.5
75% max
Distrito
Arganzuela 4064.75 5729.0
Barajas 3326.50 4415.0
Carabanchel 2608.00 3308.0
Centro 4989.00 7253.0
Chamartín 5102.00 7369.0
Chamberí 5314.25 8333.0
Ciudad Lineal 3255.25 4606.0
Fuencarral 3577.50 4898.0
Hortaleza 3739.00 5031.0
Latina 2554.25 3638.0
Moncloa 3992.50 5811.0
Moratalaz 2621.00 3902.0
Puente de Vallecas 2371.25 2944.0
Retiro 4632.75 7295.0
Salamanca 5801.25 9818.0
San Blas 2884.50 3656.0
Tetuán 3706.75 5430.0
Usera 2375.00 3127.0
Vicálvaro 2599.00 3426.0
Villa de Vallecas 2654.00 3275.0
Villaverde 2049.75 2900.0
import matplotlib.pyplot as plt
# Aseguramos que la columna fecha está en formato datetime
hist_precios_madrid["Mes"] = pd.to_datetime(hist_precios_madrid["Mes"], errors="coerce")
# Filtrar solo la fecha 01/04/2025
filtro = hist_precios_madrid[hist_precios_madrid["Mes"] == "2025-04-01"]
# Agrupar por distrito y calcular el precio medio ese día
precios_distrito = filtro.groupby("Distrito")["Precio m2"].mean().sort_values()
# Graficar
plt.figure(figsize=(12,6))
precios_distrito.plot(kind="bar")
plt.title("Precio medio por m² en Madrid por distrito (01/04/2025)", fontsize=14)
plt.ylabel("Precio m² (€)", fontsize=12)
plt.xlabel("Distrito", fontsize=12)
plt.xticks(rotation=75)
plt.tight_layout()
plt.show()
import matplotlib.pyplot as plt
# Aseguramos que "Precio m2" es numérico
hist_precios_madrid["Precio m2"] = pd.to_numeric(
hist_precios_madrid["Precio m2"], errors="coerce"
)
# Boxplot para toda la serie temporal (varios años)
plt.figure(figsize=(30,40))
hist_precios_madrid.boxplot(column="Precio m2", by="Distrito", grid=False, rot=75)
plt.title("Distribución histórica del Precio por m² en Madrid (por distrito)")
plt.suptitle("") # eliminar el título extra
plt.ylabel("Precio m² (€)")
plt.xlabel("Distrito")
plt.tight_layout()
plt.show()
<Figure size 3000x4000 with 0 Axes>
Nº de outliers¶
# Aseguramos que Precio m2 es numérico
hist_precios_madrid["Precio m2"] = pd.to_numeric(hist_precios_madrid["Precio m2"], errors="coerce")
outliers_count = {}
# Recorremos distrito a distrito
for distrito, datos in hist_precios_madrid.groupby("Distrito"):
serie = datos["Precio m2"].dropna()
q1 = serie.quantile(0.25)
q3 = serie.quantile(0.75)
iqr = q3 - q1
lower = q1 - 1.5 * iqr
upper = q3 + 1.5 * iqr
outliers = serie[(serie < lower) | (serie > upper)]
outliers_count[distrito] = len(outliers)
# Convertimos a DataFrame y lo mostramos
outliers_df = pd.DataFrame.from_dict(outliers_count, orient="index", columns=["N_outliers"]).sort_values("N_outliers", ascending=False)
outliers_df
| N_outliers | |
|---|---|
| Salamanca | 12 |
| Chamartín | 11 |
| Moncloa | 10 |
| Tetuán | 8 |
| Chamberí | 7 |
| Retiro | 7 |
| Fuencarral | 5 |
| Moratalaz | 4 |
| Centro | 3 |
| Ciudad Lineal | 3 |
| Hortaleza | 3 |
| Arganzuela | 2 |
| Barajas | 2 |
| Villaverde | 2 |
| Latina | 1 |
| Carabanchel | 0 |
| Puente de Vallecas | 0 |
| San Blas | 0 |
| Usera | 0 |
| Vicálvaro | 0 |
| Villa de Vallecas | 0 |
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import math
# Asegura tipos
hist_precios_madrid["Mes"] = pd.to_datetime(hist_precios_madrid["Mes"], errors="coerce")
hist_precios_madrid["Precio m2"] = pd.to_numeric(hist_precios_madrid["Precio m2"], errors="coerce")
# Sin filtro (todo el histórico):
hist = hist_precios_madrid.copy()
# Lista de distritos
distritos = sorted(hist["Distrito"].dropna().unique())
# Parámetros del panel
n = len(distritos)
ncols = 5
nrows = math.ceil(n / ncols)
serie_global = hist["Precio m2"].dropna()
xmin = np.nanpercentile(serie_global, 1)
xmax = np.nanpercentile(serie_global, 99)
# Crear figura
fig, axes = plt.subplots(nrows=nrows, ncols=ncols, figsize=(ncols*4, nrows*3), squeeze=False)
for i, distrito in enumerate(distritos):
r, c = divmod(i, ncols)
ax = axes[r, c]
s = hist.loc[hist["Distrito"] == distrito, "Precio m2"].dropna()
ax.hist(s, bins=20)
ax.set_title(f"{distrito} (n={len(s)})", fontsize=10)
ax.set_xlim(xmin, xmax)
ax.tick_params(axis='x', labelrotation=0)
# Línea de mediana para cada distrito
if len(s) > 0:
med = float(np.nanmedian(s))
ax.axvline(med, linestyle='--')
# Quitar ejes vacíos si el grid es mayor que n
for j in range(i+1, nrows*ncols):
r, c = divmod(j, ncols)
axes[r, c].axis('off')
fig.suptitle("Histogramas del Precio por m² por distrito (histórico)", fontsize=14)
fig.tight_layout(rect=[0, 0, 1, 0.96])
plt.show()
📌 Observaciones destacadas
Distritos de gama alta:
Salamanca, Chamartín, Chamberí, Retiro, Centro → distribuciones centradas en precios altos (≈4.000–6.000 €/m², con colas hasta 8.000–9.000).
Además, presentan cola derecha larga: hay pisos con precios extraordinariamente altos (probablemente viviendas de lujo o anuncios con sesgo).
Distritos intermedios:
Moncloa, Arganzuela, Ciudad Lineal, Hortaleza, Tetuán, Latina → rangos medios, entre 3.000–4.500 €/m².
Distribuciones con cierta dispersión → reflejan mezcla de barrios caros y otros más asequibles dentro del mismo distrito.
Distritos más asequibles:
Villaverde, Puente de Vallecas, Villa de Vallecas, Usera, San Blas, Vicálvaro → la mayor parte de los precios está en el rango 1.800–3.000 €/m².
Distribuciones más concentradas, indicando un mercado más homogéneo.
Barajas y Moratalaz:
Muestran distribuciones más estrechas en torno a 2.500–3.500 €/m².
Sugieren estabilidad y menos extremos en comparación con otros distritos.
🧠 Insight
Evidente polarización del mercado inmobiliario en Madrid: distritos de lujo con precios muy elevados y dispersos frente a zonas periféricas con precios más bajos y homogéneos.
La asimetría (colas largas hacia la derecha) indica que existen propiedades premium que tiran hacia arriba las medias en distritos caros → conviene reportar medianas además de medias para no sesgar el análisis.
Este panel refuerza la narrativa de la desigualdad geográfica en el acceso a vivienda en Madrid.
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.cm as cm
import numpy as np
# Diccionario de dataframes por distrito
distritos = {
"Arganzuela": df_arganzuela,
"Barajas": df_barajas,
"Carabanchel": df_carabanchel,
"Centro": df_centro,
"Chamartín": df_chamartin,
"Chamberí": df_chamberi,
"Ciudad Lineal": df_ciudad_lineal,
"Fuencarral": df_fuencarral,
"Hortaleza": df_hortaleza,
"Latina": df_latina,
"Moncloa": df_moncloa,
"Moratalaz": df_moratalaz,
"Puente de Vallecas": df_puente_de_vallecas,
"Retiro": df_retiro,
"Salamanca": df_salamanca,
"San Blas": df_san_blas,
"Tetuán": df_tetuan,
"Usera": df_usera,
"Vicálvaro": df_vicalvaro,
"Villa de Vallecas": df_villa_de_vallecas,
"Villaverde": df_villaverde
}
# Paleta de colores más clara y visual
colormap = cm.get_cmap('tab20', len(distritos))
# Crear gráfico
plt.figure(figsize=(14, 10))
for i, (nombre, df) in enumerate(distritos.items()):
df = df.copy()
df['Mes'] = pd.to_datetime(df['Mes'])
df.set_index('Mes', inplace=True)
df['Precio m2'] = pd.to_numeric(df['Precio m2'], errors='coerce')
# Plot con color diferenciado
plt.plot(df.index, df['Precio m2'], label=nombre, color=colormap(i),linewidth=2)
# Personalización
plt.title("Evolución anual del Precio m² por Distrito", fontsize=14)
plt.xlabel("Año")
plt.ylabel("Precio m²")
plt.legend(loc='center left', bbox_to_anchor=(1, 0.5), fontsize=9)
plt.grid(True)
plt.tight_layout()
plt.show()
C:\Users\evahr\AppData\Local\Temp\ipykernel_23052\3319063319.py:32: MatplotlibDeprecationWarning: The get_cmap function was deprecated in Matplotlib 3.7 and will be removed in 3.11. Use ``matplotlib.colormaps[name]`` or ``matplotlib.colormaps.get_cmap()`` or ``pyplot.get_cmap()`` instead.
colormap = cm.get_cmap('tab20', len(distritos)) # 20 colores bien diferenciados
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.cm as cm
import numpy as np
# Diccionario de dataframes por distrito
distritos = {
"Arganzuela": df_arganzuela,
"Barajas": df_barajas,
"Carabanchel": df_carabanchel,
"Centro": df_centro,
"Chamartín": df_chamartin,
"Chamberí": df_chamberi,
"Ciudad Lineal": df_ciudad_lineal,
"Fuencarral": df_fuencarral,
"Hortaleza": df_hortaleza,
"Latina": df_latina,
"Moncloa": df_moncloa,
"Moratalaz": df_moratalaz,
"Puente de Vallecas": df_puente_de_vallecas,
"Retiro": df_retiro,
"Salamanca": df_salamanca,
"San Blas": df_san_blas,
"Tetuán": df_tetuan,
"Usera": df_usera,
"Vicálvaro": df_vicalvaro,
"Villa de Vallecas": df_villa_de_vallecas,
"Villaverde": df_villaverde
}
# Paleta de colores más clara y visual
colormap = cm.get_cmap('tab20', len(distritos))
# Crear gráfico
plt.figure(figsize=(14, 10))
for i, (nombre, df) in enumerate(distritos.items()):
df = df.copy()
df['Mes'] = pd.to_datetime(df['Mes'])
df.set_index('Mes', inplace=True)
df['Precio m2'] = pd.to_numeric(df['Variación anual'], errors='coerce')
# Plot con color diferenciado
plt.plot(df.index, df['Precio m2'], label=nombre, color=colormap(i),linewidth=2)
# Personalización
plt.title("Evolución anual de la variación anual por Distrito", fontsize=14)
plt.xlabel("Año")
plt.ylabel("Variación anual")
plt.legend(loc='center left', bbox_to_anchor=(1, 0.5), fontsize=9)
plt.grid(True)
plt.tight_layout()
plt.show()
C:\Users\evahr\AppData\Local\Temp\ipykernel_23052\209620299.py:32: MatplotlibDeprecationWarning: The get_cmap function was deprecated in Matplotlib 3.7 and will be removed in 3.11. Use ``matplotlib.colormaps[name]`` or ``matplotlib.colormaps.get_cmap()`` or ``pyplot.get_cmap()`` instead.
colormap = cm.get_cmap('tab20', len(distritos)) # 20 colores bien diferenciados
2. Distritos latitud - longitud¶
lat_long = pd.read_csv(r"C:\Users\evahr\Downloads\TFM-idealista\Distritos_Lat_Lon.csv", sep=';')
lat_long.head(10)
| DISTRITO | LATITUD | LONGITUD | |
|---|---|---|---|
| 0 | Centro | 40.41831 | -3.70275 |
| 1 | Arganzuela | 40.40021 | -3.69618 |
| 2 | Retiro | 40.41317 | -3.68307 |
| 3 | Salamanca | 40.42972 | -3.67975 |
| 4 | Chamartín | 40.46206 | -3.67660 |
| 5 | Tetuan | 40.45975 | -3.69750 |
| 6 | Chamberi | 40.43404 | -3.70379 |
| 7 | Fuencarral-El Pardo | 40.4984 | -3.73140 |
| 8 | Moncloa-Aravaca | 40.43547 | -3.73170 |
| 9 | Latina | 40.38897 | -3.74569 |
3. Distritos¶
distritos = pd.read_excel(r"C:\Users\evahr\Downloads\TFM-idealista\Distritos.xlsx")
distritos.rename(columns={'NOMBRE': 'DISTRITO'}, inplace=True)
distritos.head(10)
| Shape_Leng | Shape_Area | COD_DIS | COD_DIS_TX | DISTRITO | DISTRI_MAY | DISTRI_MT | FCH_ALTA | FCH_BAJA | OBSERVACIONES | ACUERDO | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 10304.082346 | 5.228246e+06 | 1 | 1 | Centro | CENTRO | CENTRO | NaN | NaN | NaN | NaN |
| 1 | 12806.765287 | 6.462176e+06 | 2 | 2 | Arganzuela | ARGANZUELA | ARGANZUELA | NaN | NaN | NaN | NaN |
| 2 | 9523.887357 | 5.466211e+06 | 3 | 3 | Retiro | RETIRO | RETIRO | NaN | NaN | NaN | NaN |
| 3 | 10866.335995 | 5.392404e+06 | 4 | 4 | Salamanca | SALAMANCA | SALAMANCA | NaN | NaN | NaN | NaN |
| 4 | 13396.817759 | 9.175482e+06 | 5 | 5 | Chamartín | CHAMARTIN | CHAMARTÍN | NaN | NaN | NaN | NaN |
| 5 | 9919.063144 | 5.374725e+06 | 6 | 6 | Tetuan | TETUAN | TETUÁN | NaN | NaN | NaN | NaN |
| 6 | 9019.544152 | 4.679185e+06 | 7 | 7 | Chamberi | CHAMBERI | CHAMBERÍ | NaN | NaN | NaN | NaN |
| 7 | 94480.909361 | 2.378383e+08 | 8 | 8 | Fuencarral-El Pardo | FUENCARRAL - EL PARDO | FUENCARRAL - EL PARDO | NaN | NaN | NaN | NaN |
| 8 | 41016.912066 | 4.658695e+07 | 9 | 9 | Moncloa-Aravaca | MONCLOA - ARAVACA | MONCLOA - ARAVACA | NaN | NaN | NaN | NaN |
| 9 | 28142.972535 | 2.541039e+07 | 10 | 10 | Latina | LATINA | LATINA | NaN | NaN | NaN | NaN |
4. Precio viviendas distritos¶
precio_distritos = pd.read_excel(r"C:\Users\evahr\Downloads\TFM-idealista\Precio_viviendas_distritos.xlsx")
precio_distritos.head(10)
| Año | Distrito | Enero | Febrero | Marzo | Abril | Mayo | Junio | Julio | Agosto | Septiembre | Octubre | Noviembre | Diciembre | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2006 | Ciudad de Madrid | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. |
| 1 | 2006 | Centro | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. |
| 2 | 2006 | Arganzuela | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. |
| 3 | 2006 | Retiro | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. |
| 4 | 2006 | Salamanca | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. |
| 5 | 2006 | Chamartín | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. |
| 6 | 2006 | Tetuán | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. |
| 7 | 2006 | Chamberí | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. |
| 8 | 2006 | Fuencarral-El Pardo | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. |
| 9 | 2006 | Moncloa-Aravaca | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. |
5. Ranking vulnerabilidad distritos¶
ranking = pd.read_excel(r"C:\Users\evahr\Downloads\TFM-idealista\Ranking_Vulnerabilidad_Distritos.xlsx")
ranking.head(10)
| Año | Ranking | Distrito | Vulnerabilidad | |
|---|---|---|---|---|
| 0 | 2020 | 1 | Puente de Vallecas | 0.011033 |
| 1 | 2020 | 2 | Villaverde | 0.010210 |
| 2 | 2020 | 3 | Usera | 0.010118 |
| 3 | 2020 | 4 | Carabanchel | 0.009590 |
| 4 | 2020 | 5 | Latina | 0.008578 |
| 5 | 2020 | 6 | Villa de Vallecas | 0.008818 |
| 6 | 2020 | 7 | San Blas-Canillejas | 0.008328 |
| 7 | 2020 | 8 | Tetuán | 0.008002 |
| 8 | 2020 | 9 | Moratalaz | 0.007922 |
| 9 | 2020 | 10 | Vicálvaro | 0.008100 |
# Aseguramos que la columna vulnerabilidad se identifica (busca por nombre)
col_vuln = next(c for c in ranking.columns if 'vulner' in str(c).lower())
# Filtrar año 2020
ranking_2020 = ranking[ranking["Año"] == 2020].copy()
# Limpiar nombres de distrito
ranking_2020["Distrito"] = (
ranking_2020["Distrito"]
.astype(str)
.str.replace(r'^\s*\d+\s*', '', regex=True)
.str.strip()
)
# Ordenar de mayor a menor vulnerabilidad
ranking_2020 = ranking_2020.sort_values(col_vuln, ascending=False)
# Gráfico de barras
plt.figure(figsize=(12,6))
plt.bar(ranking_2020["Distrito"], ranking_2020[col_vuln])
plt.title("Índice de vulnerabilidad por distrito (2020)")
plt.ylabel("Índice de vulnerabilidad")
plt.xlabel("Distrito")
plt.xticks(rotation=75)
plt.tight_layout()
plt.show()
6. Histórico Euribor¶
euribor = pd.read_excel(r"C:\Users\evahr\Downloads\TFM-idealista\Euribor Histórico.xlsx")
euribor.head(10)
| Año | Mes | Valor | |
|---|---|---|---|
| 0 | 2025 | Agosto | 0.02130 |
| 1 | 2025 | Julio | 0.02079 |
| 2 | 2025 | Junio | 0.02081 |
| 3 | 2025 | Mayo | 0.02080 |
| 4 | 2025 | Abril | 0.02143 |
| 5 | 2025 | Marzo | 0.02398 |
| 6 | 2025 | Febrero | 0.02402 |
| 7 | 2025 | Enero | 0.02526 |
| 8 | 2024 | Diciembre | 0.02433 |
| 9 | 2024 | Noviembre | 0.02506 |
# Diccionario para pasar de nombre de mes a número
meses = {
"Enero": 1, "Febrero": 2, "Marzo": 3, "Abril": 4,
"Mayo": 5, "Junio": 6, "Julio": 7, "Agosto": 8,
"Septiembre": 9, "Octubre": 10, "Noviembre": 11, "Diciembre": 12
}
euribor = euribor.copy()
euribor["Mes_num"] = euribor["Mes"].map(meses)
# Crear columna de fecha (primer día de cada mes)
euribor["Fecha"] = pd.to_datetime(dict(year=euribor["Año"], month=euribor["Mes_num"], day=1))
# Ordenar por fecha
euribor = euribor.sort_values("Fecha")
# Graficar
plt.figure(figsize=(12,6))
plt.plot(euribor["Fecha"], euribor["Valor"], marker="o")
plt.title("Evolución histórica del Euribor")
plt.xlabel("Fecha")
plt.ylabel("Euribor")
plt.grid(True)
plt.tight_layout()
plt.show()
7. Transmisiones de propiedad de viviendas a través de compraventa en Comunidad de Madrid¶
transmisiones = pd.read_excel(r"C:\Users\evahr\Downloads\TFM-idealista\Transmisiones de propiedad de viviendas a través de compraventa en Comunidad de Madrid.xlsx")
transmisiones.head(10)
| Año | Mes | Dato | T1_1 | T1_12 | |
|---|---|---|---|---|---|
| 0 | 2007-01-01 00:00:00 | enero | 7978 | NaN | NaN |
| 1 | 2007-01-02 00:00:00 | febrero | 8214 | 3 | NaN |
| 2 | 2007-01-03 00:00:00 | marzo | 8174 | -0.5 | NaN |
| 3 | 2007-01-04 00:00:00 | abril | 6973 | -14.7 | NaN |
| 4 | 2007-01-05 00:00:00 | mayo | 7678 | 10.1 | NaN |
| 5 | 2007-01-06 00:00:00 | junio | 7504 | -2.3 | NaN |
| 6 | 2007-01-07 00:00:00 | julio | 7309 | -2.6 | NaN |
| 7 | 2007-01-08 00:00:00 | agosto | 6590 | -9.8 | NaN |
| 8 | 2007-01-09 00:00:00 | septiembre | 7528 | 14.2 | NaN |
| 9 | 2007-01-10 00:00:00 | octubre | 6347 | -15.7 | NaN |
df = transmisiones.copy()
# Diccionario meses en español → número
meses = {
"enero": 1, "febrero": 2, "marzo": 3, "abril": 4,
"mayo": 5, "junio": 6, "julio": 7, "agosto": 8,
"septiembre": 9, "octubre": 10, "noviembre": 11, "diciembre": 12
}
# Año como entero
df["Año"] = pd.to_datetime(df["Año"], errors="coerce").dt.year
# Crear columna fecha
df["Mes_num"] = df["Mes"].map(meses)
df["Fecha"] = pd.to_datetime(dict(year=df["Año"], month=df["Mes_num"], day=1))
# 🔹 Limpiar la columna Dato (quitar todo lo que no sea número o punto)
df["Dato"] = df["Dato"].astype(str).str.replace(r"[^\d.,-]", "", regex=True)
# Convertir a float (cambiando comas por puntos si hay)
df["Dato"] = df["Dato"].str.replace(",", ".", regex=False)
df["Dato"] = pd.to_numeric(df["Dato"], errors="coerce")
# Ordenar
df = df.sort_values("Fecha")
# Gráfico de líneas
plt.figure(figsize=(14,6))
plt.plot(df["Fecha"], df["Dato"], marker="o", linewidth=1)
plt.title("Transmisiones de propiedad de viviendas en la Comunidad de Madrid")
plt.xlabel("Fecha")
plt.ylabel("Número de transmisiones")
plt.grid(True, linestyle="--", alpha=0.6)
plt.tight_layout()
plt.show()
8. índice de precios de vivienda en la Comunidad de Madrid¶
precios_vivienda = pd.read_excel(r"C:\Users\evahr\Downloads\TFM-idealista\Índice de Precios de Vivienda en Comunidad de Madrid.xlsx")
precios_vivienda.head(10)
| Año | Trimestre | Dato | T1_1 | T1_4 | |
|---|---|---|---|---|---|
| 0 | 2007-01-01 | 1º trimestre | 157.23 | NaN | NaN |
| 1 | 2007-01-02 | 2º trimestre | 160.67 | 2.2 | NaN |
| 2 | 2007-01-03 | 3º trimestre | 161.26 | 0.4 | NaN |
| 3 | 2007-01-04 | 4º trimestre | 154.51 | -4.2 | NaN |
| 4 | 2008-01-01 | 1º trimestre | 152.90 | -1.0 | -2.8 |
| 5 | 2008-01-02 | 2º trimestre | 153.00 | 0.1 | -4.8 |
| 6 | 2008-01-03 | 3º trimestre | 149.95 | -2.0 | -7.0 |
| 7 | 2008-01-04 | 4º trimestre | 141.98 | -5.3 | -8.1 |
| 8 | 2009-01-01 | 1º trimestre | 135.80 | -4.4 | -11.2 |
| 9 | 2009-01-02 | 2º trimestre | 134.05 | -1.3 | -12.4 |
pv = precios_vivienda.copy()
# Diccionario: trimestre → mes representativo
trimestres = {
"1º trimestre": 1,
"2º trimestre": 4,
"3º trimestre": 7,
"4º trimestre": 10
}
# Crear columna de fecha combinando año + mes representativo
pv["Mes_num"] = pv["Trimestre"].map(trimestres)
pv["Fecha"] = pd.to_datetime(dict(year=pd.to_datetime(pv["Año"]).dt.year,
month=pv["Mes_num"], day=1))
# Ordenar por fecha
pv = pv.sort_values("Fecha")
# Gráfico de líneas
plt.figure(figsize=(12,6))
plt.plot(pv["Fecha"], pv["Dato"], marker="o", linewidth=2)
plt.title("Evolución del precio medio de la vivienda en la Comunidad de Madrid")
plt.xlabel("Año")
plt.ylabel("Precio medio (€/m²)")
plt.grid(True, linestyle="--", alpha=0.6)
plt.tight_layout()
plt.show()
9. Capital total hipotecado en la Comunidad de Madrid¶
capital_hipotecado = pd.read_excel(r"C:\Users\evahr\Downloads\TFM-idealista\Capital total hipotecado en Comunidad de Madrid.xlsx")
capital_hipotecado.head(10)
| AÑO | MES | DATO | T1_1 | T1_12 | |
|---|---|---|---|---|---|
| 0 | 2007-01-01 | enero | 4303037.0 | NaN | NaN |
| 1 | 2007-01-02 | febrero | 3929815.0 | -8.7 | NaN |
| 2 | 2007-01-03 | marzo | 3837192.0 | -2.4 | NaN |
| 3 | 2007-01-04 | abril | 3257875.0 | -15.1 | NaN |
| 4 | 2007-01-05 | mayo | 3911680.0 | 20.1 | NaN |
| 5 | 2007-01-06 | junio | 3733810.0 | -4.5 | NaN |
| 6 | 2007-01-07 | julio | 3874075.0 | 3.8 | NaN |
| 7 | 2007-01-08 | agosto | 3077892.0 | -20.6 | NaN |
| 8 | 2007-01-09 | septiembre | 3740588.0 | 21.5 | NaN |
| 9 | 2007-01-10 | octubre | 3433042.0 | -8.2 | NaN |
ch = capital_hipotecado.copy()
# Diccionario meses en español → número
meses = {
"enero": 1, "febrero": 2, "marzo": 3, "abril": 4,
"mayo": 5, "junio": 6, "julio": 7, "agosto": 8,
"septiembre": 9, "octubre": 10, "noviembre": 11, "diciembre": 12
}
# Extraer año como entero
ch["AÑO"] = pd.to_datetime(ch["AÑO"], errors="coerce").dt.year
# Crear columna de fecha
ch["Mes_num"] = ch["MES"].map(meses)
ch["Fecha"] = pd.to_datetime(dict(year=ch["AÑO"], month=ch["Mes_num"], day=1))
# Ordenar por fecha
ch = ch.sort_values("Fecha")
# Gráfico de líneas
plt.figure(figsize=(14,6))
plt.plot(ch["Fecha"], ch["DATO"], marker="o", linewidth=1.5)
plt.title("Capital total hipotecado en la Comunidad de Madrid")
plt.xlabel("Fecha")
plt.ylabel("Capital hipotecado (€)")
plt.grid(True, linestyle="--", alpha=0.6)
plt.tight_layout()
plt.show()
10. Total hipotecas constituidas en Madrid mensualmente¶
total_hipotecas = pd.read_excel(r"C:\Users\evahr\Downloads\TFM-idealista\Total hipotecas constituidas en Madrid mensualmente.xlsx")
total_hipotecas.head(10)
| FECHA | MES | DATO | T1_1 | T1_12 | |
|---|---|---|---|---|---|
| 0 | 2007-01-01 | enero | 18622.0 | NaN | NaN |
| 1 | 2007-01-02 | febrero | 16065.0 | -13.7 | NaN |
| 2 | 2007-01-03 | marzo | 19344.0 | 20.4 | NaN |
| 3 | 2007-01-04 | abril | 15919.0 | -17.7 | NaN |
| 4 | 2007-01-05 | mayo | 17050.0 | 7.1 | NaN |
| 5 | 2007-01-06 | junio | 16133.0 | -5.4 | NaN |
| 6 | 2007-01-07 | julio | 16479.0 | 2.1 | NaN |
| 7 | 2007-01-08 | agosto | 13744.0 | -16.6 | NaN |
| 8 | 2007-01-09 | septiembre | 17779.0 | 29.4 | NaN |
| 9 | 2007-01-10 | octubre | 13448.0 | -24.4 | NaN |
th = total_hipotecas.copy()
# 1) Año correcto a partir de la columna Fecha original
th["AÑO"] = pd.to_datetime(th["FECHA"], errors="coerce").dt.year
# 2) Mapear nombre de mes → número de mes
meses = {
"enero": 1, "febrero": 2, "marzo": 3, "abril": 4,
"mayo": 5, "junio": 6, "julio": 7, "agosto": 8,
"septiembre": 9, "octubre": 10, "noviembre": 11, "diciembre": 12
}
th["Mes_num"] = th["MES"].str.lower().map(meses)
# 3) Construir la fecha mensual correcta (primer día del mes)
th["Fecha_ok"] = pd.to_datetime(dict(year=th["AÑO"], month=th["Mes_num"], day=1))
# 4) (Opcional) si hubiera duplicados por mes, agrupa (media)
th = th.groupby("Fecha_ok", as_index=False)["DATO"].mean()
# 5) Ordenar y graficar
th = th.sort_values("Fecha_ok")
plt.figure(figsize=(14,6))
plt.plot(th["Fecha_ok"], th["DATO"], marker="o", linewidth=1.5)
plt.title("Total de hipotecas constituidas en Madrid (mensual)")
plt.xlabel("Fecha")
plt.ylabel("Número de hipotecas")
plt.grid(True, linestyle="--", alpha=0.6)
plt.tight_layout()
plt.show()
11. Población activa¶
poblacion_activa = pd.read_excel(r"C:\Users\evahr\Downloads\TFM-idealista\Población activa.xlsx")
poblacion_activa.head(10)
| AÑO | TRIMESTRE | DATO | T1_1 | T1_4 | |
|---|---|---|---|---|---|
| 0 | 2007-01-01 | 1º trimestre | 3307.9 | NaN | NaN |
| 1 | 2007-01-02 | 2º trimestre | 3314.8 | 0.21 | NaN |
| 2 | 2007-01-03 | 3º trimestre | 3351.9 | 1.12 | NaN |
| 3 | 2007-01-04 | 4º trimestre | 3365.3 | 0.40 | NaN |
| 4 | 2008-01-01 | 1º trimestre | 3375.6 | 0.31 | 2.05 |
| 5 | 2008-01-02 | 2º trimestre | 3425.8 | 1.49 | 3.35 |
| 6 | 2008-01-03 | 3º trimestre | 3436.6 | 0.32 | 2.53 |
| 7 | 2008-01-04 | 4º trimestre | 3468.9 | 0.94 | 3.08 |
| 8 | 2009-01-01 | 1º trimestre | 3481.8 | 0.37 | 3.15 |
| 9 | 2009-01-02 | 2º trimestre | 3481.4 | -0.01 | 1.62 |
pa = poblacion_activa.copy()
# Mapear trimestre → mes representativo (inicio de trimestre)
trimestres = {
"1º trimestre": 1,
"2º trimestre": 4,
"3º trimestre": 7,
"4º trimestre": 10,
}
# Año como entero y fecha trimestral correcta
pa["AÑO"] = pd.to_datetime(pa["AÑO"], errors="coerce").dt.year
pa["Mes_num"] = pa["TRIMESTRE"].str.lower().map(trimestres)
pa["Fecha"] = pd.to_datetime(dict(year=pa["AÑO"], month=pa["Mes_num"], day=1))
# Asegurar numérico y ordenar
pa["DATO"] = pd.to_numeric(pa["DATO"], errors="coerce")
pa = pa.dropna(subset=["Fecha", "DATO"]).sort_values("Fecha")
# Media móvil 4 trimestres (tendencia)
pa["MM4"] = pa["DATO"].rolling(window=4, min_periods=1).mean()
# --- Gráfico ---
plt.figure(figsize=(12,6))
plt.plot(pa["Fecha"], pa["DATO"], marker="o", linewidth=1, label="Población activa (trimestral)")
plt.plot(pa["Fecha"], pa["MM4"], linewidth=2, label="Media móvil 4 trimestres")
plt.title("Población activa (trimestral) – Comunidad de Madrid")
plt.xlabel("Fecha")
plt.ylabel("Población activa")
plt.grid(True, linestyle="--", alpha=0.6)
plt.legend()
plt.tight_layout()
plt.show()
12. Renta disponible¶
renta = pd.read_excel(r"C:\Users\evahr\Downloads\TFM-idealista\Renta disponible.xlsx")
renta.head(10)
| Dato | Año | Ciudad de Madrid | Centro | Arganzuela | Retiro | Salamanca | Chamartín | Tetuán | Chamberí | ... | Usera | Puente de Vallecas | Moratalaz | Ciudad Lineal | Hortaleza | Villaverde | Villa de Vallecas | Vicálvaro | San Blas-Canillejas | Barajas | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Renta neta media por persona | 2015 | 15.257 | 15.512 | 16.855 | 20.900 | 23.009 | 24.155 | 14.563 | 21.558 | ... | 9.276 | 9.386 | 13.714 | 14.767 | 17.662 | 9.642 | 11.688 | 11.375 | 13.171 | 17.257 |
| 1 | Renta neta media por persona | 2016 | 15.717 | 16.147 | 17.306 | 21.504 | 24.433 | 25.969 | 14.970 | 22.499 | ... | 9.395 | 9.545 | 13.944 | 15.048 | 18.277 | 9.756 | 11.925 | 11.695 | 13.404 | 17.641 |
| 2 | Renta neta media por persona | 2017 | 15.930 | 16.711 | 17.738 | 21.598 | 24.683 | 26.267 | 15.180 | 22.897 | ... | 9.552 | 9.706 | 14.135 | 15.111 | 18.620 | 9.875 | 12.114 | 11.941 | 13.559 | 17.807 |
| 3 | Renta neta media por persona | 2018 | 16.700 | 17.932 | 18.473 | 22.706 | 26.255 | 28.190 | 15.742 | 24.112 | ... | 9.860 | 10.045 | 14.637 | 15.815 | 19.759 | 10.164 | 12.545 | 12.353 | 14.100 | 18.514 |
| 4 | Renta neta media por persona | 2019 | 17.030 | 18.789 | 19.088 | 23.262 | 25.770 | 27.634 | 16.354 | 24.881 | ... | 10.211 | 10.402 | 15.101 | 16.349 | 20.044 | 10.540 | 12.976 | 12.916 | 14.545 | 19.026 |
| 5 | Renta neta media por persona | 2020 | 17.059 | 18.314 | 19.284 | 23.227 | 25.932 | 27.719 | 16.336 | 24.913 | ... | 10.239 | 10.445 | 15.294 | 16.436 | 19.890 | 10.591 | 13.099 | 13.123 | 14.537 | 19.138 |
| 6 | Renta neta media por persona | 2021 | 17.586 | 19.199 | 20.103 | 23.925 | 25.956 | 28.233 | 17.026 | 25.275 | ... | 10.797 | 10.941 | 15.667 | 16.890 | 20.500 | 11.059 | 13.605 | 13.849 | 15.115 | 19.482 |
| 7 | Renta neta media por persona | 2022 | 18.632 | 20.587 | 21.383 | 25.407 | 28.140 | 30.506 | 18.062 | 27.076 | ... | 11.430 | 11.452 | 16.437 | 17.808 | 21.766 | 11.563 | 14.274 | 14.832 | 15.902 | 20.788 |
| 8 | Renta neta media por hogar | 2015 | 38.539 | 31.392 | 40.203 | 51.795 | 53.978 | 60.962 | 34.702 | 48.997 | ... | 25.573 | 24.690 | 34.753 | 36.833 | 47.113 | 26.599 | 30.175 | 31.466 | 34.498 | 46.347 |
| 9 | Renta neta media por hogar | 2016 | 39.613 | 32.458 | 41.122 | 52.963 | 57.147 | 65.260 | 35.609 | 50.882 | ... | 25.954 | 25.029 | 35.059 | 37.421 | 48.688 | 26.915 | 30.861 | 32.152 | 35.077 | 47.389 |
10 rows × 24 columns
import math
# === 1) Preparación y limpieza ===
r = renta.copy()
# Normaliza el texto de 'Dato'
r["Dato"] = (
r["Dato"].astype(str)
.str.replace(r"\s+", " ", regex=True)
.str.strip()
)
# Extrae el AÑO de forma robusta (funciona con 2015, 2015.0, '2015-01-01', etc.)
r["Año"] = r["Año"].astype(str).str.extract(r"(\d{4})")[0]
r["Año"] = pd.to_numeric(r["Año"], errors="coerce")
# === 2) Wide -> Long ===
id_cols = ["Dato", "Año"]
dist_cols = [c for c in r.columns if c not in id_cols]
r_long = r.melt(
id_vars=id_cols,
value_vars=dist_cols,
var_name="Distrito",
value_name="Valor"
)
r_long["Valor"] = pd.to_numeric(r_long["Valor"], errors="coerce")
r_long = r_long.dropna(subset=["Valor", "Año"]).sort_values(["Dato", "Distrito", "Año"])
# Orden explícito de los 4 indicadores
orden_datos = [
"Renta neta media por persona",
"Renta neta media por hogar",
"Media de la renta neta por unidad de consumo",
"Mediana de la renta neta por unidad de consumo",
]
datos = [d for d in orden_datos if d in set(r_long["Dato"])]
# === 3) Figura 2x2: un subplot por 'Dato' ===
rows, cols = 2, 2
fig, axes = plt.subplots(nrows=rows, ncols=cols, figsize=(18, 10), sharex=False)
axes = axes.ravel()
for i, dato in enumerate(datos):
ax = axes[i]
g = r_long[r_long["Dato"] == dato]
# Una línea por distrito
for dist, gd in g.groupby("Distrito"):
ax.plot(gd["Año"].astype(int), gd["Valor"], marker="o", linewidth=1, label=dist)
ax.set_title(dato)
ax.set_xlabel("Año")
ax.set_ylabel("Valor")
ax.set_xticks(sorted(g["Año"].dropna().unique().astype(int)))
ax.grid(True, linestyle="--", alpha=0.3)
# Oculta ejes sobrantes si hubiera menos de 4 'Dato'
for j in range(len(datos), len(axes)):
axes[j].axis("off")
# === 4) Leyenda global arriba (que no tape) + títulos/márgenes ===
handles, labels = axes[0].get_legend_handles_labels()
fig.legend(
handles, labels, title="Distrito",
loc="upper center", bbox_to_anchor=(0.5, 1.20),
ncol=min(len(labels), 10), fontsize=8, frameon=False
)
fig.suptitle("Evolución por distrito (4 indicadores de renta)", y=1.12, fontsize=14)
fig.tight_layout(rect=[0, 0, 1, 0.96]) # deja margen superior para la leyenda
plt.show()
13. Población por distritos y barrios¶
poblacion_distritos = pd.read_csv(
r"C:\Users\evahr\Downloads\TFM-idealista\Población_distrito_barrio.csv",
sep=";",
encoding="utf-8"
)
print(poblacion_distritos.head(10))
fecha cod_municipio municipio cod_distrito \
0 1 de enero de 2024 28079 Madrid 1
1 1 de enero de 2024 28079 Madrid 2
2 1 de enero de 2024 28079 Madrid 3
3 1 de enero de 2024 28079 Madrid 4
4 1 de enero de 2024 28079 Madrid 5
5 1 de enero de 2024 28079 Madrid 6
6 1 de enero de 2024 28079 Madrid 7
7 1 de enero de 2024 28079 Madrid 8
8 1 de enero de 2024 28079 Madrid 9
9 1 de enero de 2024 28079 Madrid 10
distrito cod_barrio barrio num_personas \
0 Centro 1 Centro 145411
1 Arganzuela 2 Arganzuela 156559
2 Retiro 3 Retiro 119757
3 Salamanca 4 Salamanca 149778
4 Chamartín 5 Chamartín 148111
5 Tetuán 6 Tetuán 166211
6 Chamberí 7 Chamberí 141984
7 Fuencarral-El Pardo 8 Fuencarral-El Pardo 253898
8 Moncloa-Aravaca 9 Moncloa-Aravaca 125223
9 Latina 10 Latina 250396
num_personas_hombres num_personas_mujeres
0 73.734 71.677
1 73.311 83.248
2 54.339 65.418
3 66.374 83.404
4 66.860 81.251
5 76.072 90.139
6 62.918 79.066
7 119.721 134.177
8 58.121 67.102
9 116.437 133.959
import re
pd_df = poblacion_distritos.copy()
# 1) Detectar la columna de personas
val_col = next(c for c in pd_df.columns if re.search(r'num[\s_]*person', c.lower()))
# 2) Parsear la fecha (soporta "1 de enero de 2024")
pd_df["Fecha"] = pd.to_datetime(pd_df["fecha"], errors="coerce", dayfirst=True)
mask = pd_df["Fecha"].isna()
if mask.any():
meses = {
"enero":1,"febrero":2,"marzo":3,"abril":4,"mayo":5,"junio":6,
"julio":7,"agosto":8,"septiembre":9,"octubre":10,"noviembre":11,"diciembre":12
}
s = pd_df.loc[mask, "fecha"].astype(str).str.lower()
m = s.str.extract(r'(?P<dia>\d{1,2})\s+de\s+(?P<mes>[a-záéíóúüñ]+)\s+de\s+(?P<ano>\d{4})')
pd_df.loc[mask, "Fecha"] = pd.to_datetime(
dict(
year=pd.to_numeric(m["ano"], errors="coerce"),
month=m["mes"].map(meses),
day=pd.to_numeric(m["dia"], errors="coerce"),
),
errors="coerce"
)
# 3) Limpiar filas "Todos" y asegurar numérico
for col in ("distrito", "barrio", "cod_barrio"):
if col in pd_df.columns:
pd_df = pd_df[~pd_df[col].astype(str).str.lower().eq("todos")]
pd_df[val_col] = pd.to_numeric(pd_df[val_col], errors="coerce")
# 4) Filtrar a 01/01/2024 y agregar por distrito
target = pd.Timestamp(2024, 1, 1)
f = pd_df[pd_df["Fecha"] == target]
bars = (f.groupby("distrito", as_index=False)[val_col]
.sum()
.sort_values(val_col, ascending=False))
# 5) Gráfico de barras
plt.figure(figsize=(14, 7))
plt.bar(bars["distrito"], bars[val_col])
plt.title("Población por distrito (1 de enero de 2024)")
plt.xlabel("Distrito")
plt.ylabel("Personas")
plt.xticks(rotation=75)
# Etiquetas encima de cada barra (formato miles con punto)
for i, v in enumerate(bars[val_col].values):
plt.text(i, v, f"{int(round(v)):,}".replace(",", "."), ha="center", va="bottom", fontsize=8, rotation=90)
plt.tight_layout()
plt.show()
C:\Users\evahr\AppData\Local\Temp\ipykernel_23052\721619183.py:10: UserWarning: Could not infer format, so each element will be parsed individually, falling back to `dateutil`. To ensure parsing is consistent and as-expected, please specify a format. pd_df["Fecha"] = pd.to_datetime(pd_df["fecha"], errors="coerce", dayfirst=True)
14. Esperanza de vida¶
esperanza_vida = pd.read_excel(r"C:\Users\evahr\Downloads\TFM-idealista\Esperanza de vida.xlsx")
esperanza_vida.head(10)
| AÑO | Ciudad de Madrid | Centro | Arganzuela | Retiro | Salamanca | Chamartín | Tetuan | Chamberi | Fuencarral-El Pardo | ... | Usera | Puente De Vallecas | Moratalaz | Ciudad Lineal | Hortaleza | Villaverde | Villa de Vallecas | Vicalvaro | San Blas-Canillejas | Barajas | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2004 | 82 | 81,5 | 82,6 | 83 | 83 | 82,7 | 81 | 82,6 | 81,9 | ... | 81,4 | 80,2 | 82,6 | 82,4 | 82,5 | 81,3 | 79,6 | 80,4 | 81,4 | 82,3 |
| 1 | 2005 | 82 | 80,5 | 82,1 | 83,4 | 83,2 | 83,1 | 81,9 | 82,6 | 81,8 | ... | 81 | 80,5 | 82,3 | 82,5 | 81,7 | 81,4 | 81 | 80,8 | 80,6 | 82,8 |
| 2 | 2006 | 82,7 | 81,1 | 83,1 | 83,7 | 83 | 83,4 | 83,1 | 83,4 | 82,9 | ... | 81,3 | 81,3 | 83 | 82,9 | 82,9 | 82,1 | 82,7 | 83,4 | 82,1 | 83,7 |
| 3 | 2007 | 82,4 | 81,4 | 83,4 | 84,1 | 83 | 83,8 | 82,9 | 82,1 | 82,9 | ... | 81,3 | 80,7 | 82,6 | 82,6 | 82,8 | 81,5 | 81,7 | 81,4 | 81,2 | 82,3 |
| 4 | 2008 | 82,8 | 81,8 | 83,3 | 83,3 | 83,7 | 84,5 | 82,9 | 82,9 | 82,9 | ... | 81,2 | 81,3 | 82,8 | 83,7 | 83,1 | 81,7 | 81,9 | 82,6 | 82,1 | 83,5 |
| 5 | 2009 | 83,5 | 82,1 | 83,6 | 84 | 84,1 | 84,7 | 83,5 | 84,2 | 83,9 | ... | 82 | 82,6 | 84,1 | 83,8 | 83,4 | 82,8 | 82,6 | 83,1 | 83,2 | 83,3 |
| 6 | 2010 | 83,8 | 83 | 84,4 | 84,8 | 84,4 | 84,2 | 83,5 | 84 | 83,9 | ... | 83,1 | 82,3 | 83,7 | 84,2 | 84,2 | 83,3 | 83,7 | 83,3 | 82,8 | 83,3 |
| 7 | 2011 | 83,9 | 83,2 | 84,6 | 85,2 | 84,8 | 84,9 | 84 | 84,3 | 84,1 | ... | 82,2 | 82,8 | 84 | 83,8 | 83,9 | 83,6 | 83,5 | 84,4 | 83,6 | 83,8 |
| 8 | 2012 | 83,9 | 83,1 | 84,6 | 84,9 | 85,2 | 84,3 | 83,6 | 84,4 | 84 | ... | 82,9 | 83 | 83,9 | 84,6 | 84,1 | 83,3 | 83,2 | 84,4 | 83,2 | 84,2 |
| 9 | 2013 | 84,5 | 83,4 | 85,8 | 85,2 | 85,1 | 85 | 84,4 | 84,5 | 84,4 | ... | 83,5 | 83,5 | 84,5 | 85 | 85,1 | 83,6 | 83,5 | 84,2 | 83,7 | 84,5 |
10 rows × 23 columns
# === 1) Preparación ===
ev = esperanza_vida.copy()
# Extrae el año (robusto a 2015, 2015.0, '2015-01-01', etc.)
ev["AÑO"] = ev["AÑO"].astype(str).str.extract(r"(\d{4})")[0].astype("Int64")
year = 2024
fila = ev.loc[ev["AÑO"] == year]
if fila.empty:
raise ValueError(f"No hay datos para el año {year} en 'esperanza_vida'.")
# Columnas de distritos (excluye 'Año' y 'Ciudad de Madrid')
cols_distritos = [c for c in ev.columns if c not in ["AÑO", "Ciudad de Madrid"]]
# Serie distrito → valor (convierte coma decimal a punto)
s = fila[cols_distritos].iloc[0].astype(str).str.replace(",", ".", regex=False)
s = pd.to_numeric(s, errors="coerce").dropna()
# Ordena (para barras horizontales queda mejor ascendente)
s = s.sort_values()
# === 2) ELIGE ORIENTACIÓN ===
modo = "horizontal"
if modo == "vertical":
# ---- Barras verticales con zoom y etiquetas ----
vals, labels = s.values, s.index
fig, ax = plt.subplots(figsize=(14, 7))
ax.bar(labels, vals, width=0.6)
ymin = max(75, np.floor(s.min() - 1))
ymax = np.ceil(s.max() + 0.5)
ax.set_ylim(ymin, ymax)
ax.set_title(f"Esperanza de vida por distrito ({year})")
ax.set_xlabel("Distrito")
ax.set_ylabel("Años")
ax.set_xticklabels(labels, rotation=60, ha="right")
ax.grid(axis="y", linestyle="--", alpha=0.4)
# Etiquetas encima de cada barrita
for i, v in enumerate(vals):
ax.text(i, v + 0.05, f"{v:.1f}", ha="center", va="bottom", fontsize=10)
plt.tight_layout()
plt.show()
else:
# ---- Barras horizontales ----
vals, labels = s.values[::-1], s.index[::-1]
fig, ax = plt.subplots(figsize=(10, 10))
ax.barh(labels, vals)
xmin = max(75, np.floor(s.min() - 1))
xmax = np.ceil(s.max() + 0.5)
ax.set_xlim(xmin, xmax)
ax.invert_yaxis()
ax.set_title(f"Esperanza de vida por distrito ({year})")
ax.set_xlabel("Años")
ax.grid(axis="x", linestyle="--", alpha=0.4)
# Etiquetas al final de cada barra
for i, v in enumerate(vals):
ax.text(v + 0.05, i, f"{v:.1f}", va="center", ha="left", fontsize=10)
plt.tight_layout()
plt.show()
15. Paro registrado por distritos¶
paro = pd.read_excel(r"C:\Users\evahr\Downloads\TFM-idealista\Paro registrado por distritos.xlsx")
paro.head(10)
| Año | Mes | Ciudad de Madrid | 01. Centro | 01. Centro.1 | 01. Centro.2 | 01. Centro.3 | 01. Centro.4 | 01. Centro.5 | 01. Centro.6 | ... | 20. San Blas-Canillejas.7 | 20. San Blas-Canillejas.8 | 20. San Blas-Canillejas.9 | 21. Barajas | 21. Barajas.1 | 21. Barajas.2 | 21. Barajas.3 | 21. Barajas.4 | 21. Barajas.5 | 21. Barajas.6 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2017 | Enero | .. | .. | .. | .. | .. | .. | .. | .. | ... | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. |
| 1 | 2017 | Febrero | .. | .. | .. | .. | .. | .. | .. | .. | ... | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. |
| 2 | 2017 | Marzo | .. | .. | .. | .. | .. | .. | .. | .. | ... | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. |
| 3 | 2017 | Abril | .. | .. | .. | .. | .. | .. | .. | .. | ... | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. |
| 4 | 2017 | Mayo | .. | .. | .. | .. | .. | .. | .. | .. | ... | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. |
| 5 | 2017 | Junio | .. | .. | .. | .. | .. | .. | .. | .. | ... | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. |
| 6 | 2017 | Julio | .. | .. | .. | .. | .. | .. | .. | .. | ... | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. |
| 7 | 2017 | Agosto | .. | .. | .. | .. | .. | .. | .. | .. | ... | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. |
| 8 | 2017 | Septiembre | .. | .. | .. | .. | .. | .. | .. | .. | ... | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. |
| 9 | 2017 | Octubre | .. | .. | .. | .. | .. | .. | .. | .. | ... | .. | .. | .. | .. | .. | .. | .. | .. | .. | .. |
10 rows × 176 columns
# Eliminar espacios y columnas innecesarias
paro.columns = paro.columns.str.strip()
paro = paro.drop(columns=[col for col in paro.columns if col.lower() in ['mes', 'ciudad de madrid']])
# Seleccionar columnas que contienen código de distrito
columnas_distrito = [col for col in paro.columns if '.' in col]
# Convertir a numérico
paro[columnas_distrito] = paro[columnas_distrito].apply(pd.to_numeric, errors='coerce')
# Agrupar columnas por nombre base (sin sufijos .1, .2, etc.)
# Creamos un diccionario donde cada clave es el nombre base, y el valor, sus columnas
from collections import defaultdict
agrupaciones = defaultdict(list)
for col in columnas_distrito:
base = col.split('.')[0].strip() + '. ' + col.split('.')[1].split('.')[0].strip()
agrupaciones[base].append(col)
# Crear nuevo DataFrame con una columna por distrito
df_distritos = pd.DataFrame()
df_distritos["Año"] = paro["Año"]
for distrito, cols in agrupaciones.items():
df_distritos[distrito] = paro[cols].mean(axis=1)
# Agrupar por año para tener promedio anual
paro_anual = df_distritos.groupby("Año").mean()
# Graficar
plt.figure(figsize=(12, 7))
for distrito in paro_anual.columns:
plt.plot(paro_anual.index, paro_anual[distrito], label=distrito)
plt.title("Paro registrado por distrito y año")
plt.xlabel("Año")
plt.ylabel("Paro registrado promedio anual")
plt.legend(bbox_to_anchor=(1.02, 1), loc='upper left', fontsize='small')
plt.grid(True)
plt.tight_layout()
plt.show()
16. Evolución precio alquiler de la vivienda¶
evolucion_precio_alquiler = pd.read_csv(
r"C:\Users\evahr\Downloads\TFM-idealista\Evolución del precio de alquiler de la vivienda (€m2) por Distrito (Período 2006-2024).csv",
sep=";",
encoding="utf-8"
)
print(evolucion_precio_alquiler.head(10))
Año Distrito Enero Febrero Marzo Abril Mayo Junio Julio \ 0 2006 Ciudad de Madrid .. .. .. .. .. .. .. 1 2006 01. Centro .. .. .. .. .. .. .. 2 2006 02. Arganzuela .. .. .. .. .. .. .. 3 2006 03. Retiro .. .. .. .. .. .. .. 4 2006 04. Salamanca .. .. .. .. .. .. .. 5 2006 05. Chamartín .. .. .. .. .. .. .. 6 2006 06. Tetuán .. .. .. .. .. .. .. 7 2006 07. Chamberí .. .. .. .. .. .. .. 8 2006 08. Fuencarral-El Pardo .. .. .. .. .. .. .. 9 2006 09. Moncloa-Aravaca .. .. .. .. .. .. .. Agosto Septiembre Octubre Noviembre Diciembre 0 .. .. .. .. .. 1 .. .. .. .. .. 2 .. .. .. .. .. 3 .. .. .. .. .. 4 .. .. .. .. .. 5 .. .. .. .. .. 6 .. .. .. .. .. 7 .. .. .. .. .. 8 .. .. .. .. .. 9 .. .. .. .. ..
16. Evolución precio venta de la vivienda¶
evolucion_precio_venta = pd.read_excel(r"C:\Users\evahr\Downloads\TFM-idealista\Evolución precio venta vivienda.xlsx")
evolucion_precio_venta.head(10)
| Año | Distrito | Enero | Febrero | Marzo | Abril | Mayo | Junio | Julio | Agosto | Septiembre | Octubre | Noviembre | Diciembre | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2006 | Ciudad de Madrid | - | - | 3.586 | 3.431 | 3.550 | 3.473 | 3.488 | 3.477 | 3.458 | 3.359 | 3.491 | 3.487 |
| 1 | 2006 | 01. Centro | - | - | - | - | - | - | - | - | - | - | - | - |
| 2 | 2006 | 02. Arganzuela | - | - | - | - | - | - | - | - | - | - | - | - |
| 3 | 2006 | 03. Retiro | - | - | - | - | - | - | - | - | - | - | - | - |
| 4 | 2006 | 04. Salamanca | - | - | - | - | - | - | - | - | - | - | - | - |
| 5 | 2006 | 05. Chamartín | - | - | - | - | - | - | - | - | - | - | - | - |
| 6 | 2006 | 06. Tetuán | - | - | - | - | - | - | - | - | - | - | - | - |
| 7 | 2006 | 07. Chamberí | - | - | - | - | - | - | - | - | - | - | - | - |
| 8 | 2006 | 08. Fuencarral-El Pardo | - | - | - | - | - | - | - | - | - | - | - | - |
| 9 | 2006 | 09. Moncloa-Aravaca | - | - | - | - | - | - | - | - | - | - | - | - |
import re, math, numpy as np, pandas as pd
import matplotlib.pyplot as plt
# ================== CONFIG ==================
YEAR = 2024
MESES = ["Enero","Febrero","Marzo","Abril","Mayo","Junio","Julio",
"Agosto","Septiembre","Octubre","Noviembre","Diciembre"]
ABBR = ["Ene","Feb","Mar","Abr","May","Jun","Jul","Ago","Sep","Oct","Nov","Dic"]
MES2NUM = {m:i+1 for i,m in enumerate(MESES)}
# ----- Helpers -----
def to_num(x):
"""Convierte textos con miles/decimales ('.' miles, ',' decimal, '-', '..') a float."""
s = str(x).strip()
if s in ("", "-", "..", "...", "NaN", "nan", "None"):
return np.nan
s = s.replace("\u00A0","").replace(" ","")
s = s.replace(".", "")
s = s.replace(",", ".")
try: return float(s)
except: return np.nan
def clean_distrito(s):
"""Quita prefijos '01. ' etc. y deja el nombre limpio."""
return re.sub(r"^\s*\d+\.\s*", "", str(s)).strip()
def prep_wide_to_long(df, tipo_label):
"""Pasa de columnas mensuales a long, limpia numéricos y nombres."""
tmp = df.copy()
tmp["Año"] = pd.to_numeric(tmp["Año"], errors="coerce")
tmp["Distrito"] = tmp["Distrito"].apply(clean_distrito)
# columnas de meses que existan realmente en el df
month_cols = [c for c in MESES if c in tmp.columns]
long_ = tmp.melt(
id_vars=["Año","Distrito"], value_vars=month_cols,
var_name="Mes", value_name="Precio"
)
long_["Precio"] = long_["Precio"].apply(to_num)
long_["Mes_num"] = long_["Mes"].map(MES2NUM)
long_["Tipo"] = tipo_label
return long_
def plot_panel(df_long, titulo):
"""Dibuja panel: un subplot por distrito en el año YEAR."""
d = (df_long[(df_long["Año"] == YEAR) &
(df_long["Distrito"].str.lower() != "ciudad de madrid")]
.dropna(subset=["Mes_num"]))
if d.empty:
raise ValueError(f"No hay datos para {titulo} en {YEAR} tras la limpieza.")
distritos = sorted(d["Distrito"].unique())
n = len(distritos)
ncols = 5
nrows = math.ceil(n / ncols)
# mismos límites Y para todos los subplots
y_min = np.nanmin(d["Precio"])
y_max = np.nanmax(d["Precio"])
y_pad = (y_max - y_min) * 0.05 if np.isfinite(y_min) and np.isfinite(y_max) else 0
y_min, y_max = (y_min - y_pad, y_max + y_pad)
fig, axes = plt.subplots(nrows=nrows, ncols=ncols,
figsize=(ncols*4.2, nrows*3.2), sharex=True, sharey=True)
axes = np.array(axes).ravel()
for i, dist in enumerate(distritos):
ax = axes[i]
serie = d[d["Distrito"] == dist].sort_values("Mes_num")
ax.plot(serie["Mes_num"], serie["Precio"], marker="o", linewidth=1.8)
ax.set_title(dist, fontsize=10)
ax.set_xticks(range(1, 13))
ax.set_xticklabels(ABBR, fontsize=8)
ax.grid(True, linestyle="--", alpha=0.3)
ax.set_ylim(y_min, y_max)
# Oculta ejes sobrantes
for j in range(i+1, len(axes)):
axes[j].axis("off")
fig.suptitle(f"{titulo} – {YEAR}", y=1.02, fontsize=14)
fig.tight_layout(rect=[0, 0, 1, 0.98])
plt.show()
# ================== VENTA ==================
venta_long = prep_wide_to_long(evolucion_precio_venta, "Venta")
plot_panel(venta_long, "Precio de venta (€/m²) por distrito")
# ================== ALQUILER ==================
alquiler_long = prep_wide_to_long(evolucion_precio_alquiler, "Alquiler")
plot_panel(alquiler_long, "Precio de alquiler (€/m²) por distrito")
import re, math, numpy as np, pandas as pd
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
# ================== CONFIG ==================
MESES = ["Enero","Febrero","Marzo","Abril","Mayo","Junio","Julio",
"Agosto","Septiembre","Octubre","Noviembre","Diciembre"]
MES2NUM = {m: i+1 for i, m in enumerate(MESES)}
# ----- Helpers -----
def to_num(x):
"""Convierte textos con miles/decimales ('.' miles, ',' decimal, '-', '..') a float."""
s = str(x).strip()
if s in ("", "-", "..", "...", "NaN", "nan", "None"):
return np.nan
s = s.replace("\u00A0", "").replace(" ", "")
s = s.replace(".", "")
s = s.replace(",", ".")
try:
return float(s)
except Exception:
return np.nan
def clean_distrito(s):
"""Quita prefijos '01. ' etc. y deja el nombre limpio."""
return re.sub(r"^\s*\d+\.\s*", "", str(s)).strip()
def prep_wide_to_long(df, tipo_label):
"""Pasa de columnas mensuales a formato largo y limpia numéricos/nombres."""
tmp = df.copy()
tmp["Año"] = pd.to_numeric(tmp["Año"], errors="coerce")
tmp["Distrito"] = tmp["Distrito"].apply(clean_distrito)
month_cols = [c for c in MESES if c in tmp.columns]
long_ = tmp.melt(
id_vars=["Año", "Distrito"],
value_vars=month_cols,
var_name="Mes",
value_name="Precio"
)
long_["Precio"] = long_["Precio"].apply(to_num)
long_["Mes_num"] = long_["Mes"].map(MES2NUM)
long_["Tipo"] = tipo_label
# Fecha mensual (día 1)
long_["Fecha"] = pd.to_datetime(
dict(year=long_["Año"].astype("Int64"),
month=long_["Mes_num"].astype("Int64"),
day=1),
errors="coerce"
)
return long_
def plot_panel_all_years(df_long, titulo):
"""Panel: un subplot por distrito con la serie temporal completa (todos los años)."""
d = (df_long[(df_long["Distrito"].str.lower() != "ciudad de madrid")]
.dropna(subset=["Fecha", "Precio"]))
if d.empty:
raise ValueError(f"No hay datos para {titulo} tras la limpieza.")
distritos = sorted(d["Distrito"].unique())
n = len(distritos)
ncols = 5
nrows = math.ceil(n / ncols)
# misma escala Y para todo el panel
y_min = np.nanmin(d["Precio"])
y_max = np.nanmax(d["Precio"])
y_pad = (y_max - y_min) * 0.05 if np.isfinite(y_min) and np.isfinite(y_max) else 0
y_min, y_max = (y_min - y_pad, y_max + y_pad)
fig, axes = plt.subplots(nrows=nrows, ncols=ncols,
figsize=(ncols*4.4, nrows*3.2),
sharex=True, sharey=True)
axes = np.array(axes).ravel()
for i, dist in enumerate(distritos):
ax = axes[i]
serie = d[d["Distrito"] == dist].sort_values("Fecha")
ax.plot(serie["Fecha"], serie["Precio"], marker="o", linewidth=1.6)
ax.set_title(dist, fontsize=10)
ax.grid(True, linestyle="--", alpha=0.3)
ax.set_ylim(y_min, y_max)
# Ticks de año (cada 2 años; ajusta si quieres)
ax.xaxis.set_major_locator(mdates.YearLocator(base=2))
ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y"))
# Oculta celdas sobrantes
for j in range(i+1, len(axes)):
axes[j].axis("off")
fig.suptitle(titulo, y=1.02, fontsize=14)
fig.tight_layout(rect=[0, 0, 1, 0.98])
plt.show()
# ================== VENTA (todos los años) ==================
venta_long = prep_wide_to_long(evolucion_precio_venta, "Venta")
plot_panel_all_years(venta_long, "Precio de venta (€/m²) por distrito – Serie completa")
# ================== ALQUILER (todos los años) ==================
alquiler_long = prep_wide_to_long(evolucion_precio_alquiler, "Alquiler")
plot_panel_all_years(alquiler_long, "Precio de alquiler (€/m²) por distrito – Serie completa")
17. Compraventa de viviendas según régimen y provincias¶
compraventa_viviendas = pd.read_excel(r"C:\Users\evahr\Downloads\TFM-idealista\Compraventa de viviendas según régimen en Madrid.xlsx")
compraventa_viviendas.head(10)
| Régimen 28 Madrid | 2025M06 | 2025M05 | 2025M04 | 2025M03 | 2025M02 | 2025M01 | 2024M12 | 2024M11 | 2024M10 | ... | 2007M10 | 2007M09 | 2007M08 | 2007M07 | 2007M06 | 2007M05 | 2007M04 | 2007M03 | 2007M02 | 2007M01 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Viviendas: Total | 6944 | 7020 | 6604 | 7380 | 6963 | 7102 | 6010 | 6876 | 8290 | ... | 6347 | 7528 | 6590 | 7309 | 7504 | 7678 | 6973 | 8174 | 8214 | 7978 |
| 1 | Vivienda nueva | 1302 | 1559 | 1499 | 1843 | 1786 | 2225 | 1518 | 1587 | 2047 | ... | 2534 | 2869 | 2166 | 2555 | 2110 | 2400 | 2633 | 2622 | 2716 | 2514 |
| 2 | Vivienda usada | 5642 | 5461 | 5105 | 5537 | 5177 | 4877 | 4492 | 5289 | 6243 | ... | 3813 | 4659 | 4424 | 4754 | 5394 | 5278 | 4340 | 5552 | 5498 | 5464 |
| 3 | Vivienda libre | 6542 | 6651 | 6191 | 7017 | 6582 | 6734 | 5580 | 6155 | 7791 | ... | 5785 | 7175 | 6323 | 6827 | 7012 | 7193 | 6182 | 7568 | 7662 | 7555 |
| 4 | Vivienda protegida | 402 | 369 | 413 | 363 | 381 | 368 | 430 | 721 | 499 | ... | 562 | 353 | 267 | 482 | 492 | 485 | 791 | 606 | 552 | 423 |
5 rows × 223 columns
import re
import pandas as pd
import matplotlib.pyplot as plt
# --- 1) Detectar columna de régimen y columnas mensuales ---
df = compraventa_viviendas.copy()
# Columna con el nombre del régimen (por si viene con encabezados raros)
reg_col = next((c for c in df.columns if re.search(r"r[ée]gimen", str(c), flags=re.I)), df.columns[0])
df["Regimen"] = df[reg_col].astype(str).str.strip()
# Columnas tipo 'YYYYMmm' (p. ej. 2025M06)
month_cols = [c for c in df.columns if re.match(r"^\s*\d{4}M\d{2}\s*$", str(c))]
# --- 2) Wide -> long y limpieza básica ---
long = df.melt(id_vars=["Regimen"], value_vars=month_cols,
var_name="Periodo", value_name="Num")
long["Periodo"] = long["Periodo"].astype(str).str.strip()
long[["Año","Mes"]] = long["Periodo"].str.extract(r"(?P<Año>\d{4})M(?P<Mes>\d{2})").astype("Int64")
# fuerza a numérico por si hay celdas con puntos/comas/…
long["Num"] = (long["Num"].astype(str)
.str.replace(r"\s|\u00A0", "", regex=True)
.str.replace(".", "", regex=False)
.str.replace(",", ".", regex=False)
)
long["Num"] = pd.to_numeric(long["Num"], errors="coerce")
# Quitar la fila de total agregado
long = long[~long["Regimen"].str.contains("total", case=False, na=False)]
# Normalizar nombres de régimen (opcional)
long["Regimen"] = (long["Regimen"]
.str.replace(r"Vivienda\s+", "", regex=True)
.str.strip()
.str.capitalize())
# --- 3) Agregar por año y régimen (suma mensual del año) ---
anual = (long.dropna(subset=["Año"])
.groupby(["Año","Regimen"], as_index=False)["Num"]
.sum())
# Nos quedamos con los 4 regímenes clave
regimenes = ["Nueva", "Usada", "Libre", "Protegida"]
anual = anual[anual["Regimen"].isin(regimenes)]
# --- 4) Gráfico de 4 líneas (una por régimen) ---
plt.figure(figsize=(12,6))
for r in regimenes:
g = anual[anual["Regimen"] == r].sort_values("Año")
if not g.empty:
plt.plot(g["Año"], g["Num"], marker="o", linewidth=1.8, label=r)
plt.title("Compraventas de vivienda por régimen (suma anual)")
plt.xlabel("Año")
plt.ylabel("Número de viviendas")
plt.xticks(sorted(anual["Año"].unique()))
plt.grid(True, linestyle="--", alpha=0.4)
plt.legend(title="Régimen")
plt.tight_layout()
plt.show()
18. Población mensual por distritos (2022) y evolución de la población¶
poblacion_distritos = pd.read_excel(
r"C:\Users\evahr\Downloads\TFM-idealista\Población mensual por distritos.xlsx",
)
poblacion_distritos.columns = poblacion_distritos.columns.str.strip().str.upper()
print(poblacion_distritos.head(10))
DISTRITO AÑO MES POBLACIÓN MES_NUM 0 Centro 2007 Enero 141.708 1 1 Arganzuela 2007 Enero 149.951 1 2 Retiro 2007 Enero 124.507 1 3 Salamanca 2007 Enero 146.841 1 4 Chamartín 2007 Enero 143.981 1 5 Tetuán 2007 Enero 152.535 1 6 Chamberí 2007 Enero 145.570 1 7 Fuencarral-El Pardo 2007 Enero 213.547 1 8 Moncloa-Aravaca 2007 Enero 117.438 1 9 Latina 2007 Enero 256.949 1
import pandas as pd
import matplotlib.pyplot as plt
df = poblacion_distritos.copy()
# Asegura que POBLACIÓN sea numérica (maneja formatos tipo '141.708' o '141,708')
if df['POBLACIÓN'].dtype == 'O':
df['POBLACIÓN'] = (df['POBLACIÓN'].astype(str)
.str.replace('.', '', regex=False)
.str.replace(',', '.', regex=False)
.astype(float))
# Filtra diciembre 2022 (dato final del año)
dec_2022 = df[(df['AÑO'] == 2022) & (df['MES_NUM'] == 12)][['DISTRITO','POBLACIÓN']].copy()
# Ordena por población (descendente)
dec_2022 = dec_2022.sort_values('POBLACIÓN', ascending=False)
# Gráfico de barras
plt.figure(figsize=(14,7))
plt.bar(dec_2022['DISTRITO'], dec_2022['POBLACIÓN'])
plt.title('Población por distrito – Diciembre 2022')
plt.ylabel('Población')
plt.xticks(rotation=75, ha='right')
plt.tight_layout()
plt.show()
import pandas as pd
import matplotlib.pyplot as plt
df = poblacion_distritos.copy()
# --- Asegura que POBLACIÓN sea numérica (maneja '141.708' o '141,708') ---
if df['POBLACIÓN'].dtype == 'O':
df['POBLACIÓN'] = (df['POBLACIÓN'].astype(str)
.str.replace('.', '', regex=False)
.str.replace(',', '.', regex=False)
.astype(float))
# --- Tomamos el valor de fin de año por distrito (diciembre; si no hay, el último mes disponible) ---
df = df.sort_values(['AÑO', 'DISTRITO', 'MES_NUM'])
fin_de_anio = df.groupby(['AÑO', 'DISTRITO'], as_index=False).tail(1)
# --- Pivot: filas=años, columnas=distritos, valores=población de fin de año ---
pivot = fin_de_anio.pivot(index='AÑO', columns='DISTRITO', values='POBLACIÓN').sort_index()
# --- Gráfico de líneas (evolución por distrito a lo largo de todos los años) ---
ax = pivot.plot(figsize=(16, 8))
ax.set_title('Evolución de la población por distrito (fin de año)')
ax.set_xlabel('Año')
ax.set_ylabel('Población (fin de año)')
ax.legend(title='Distrito', ncol=3, fontsize=8)
plt.tight_layout()
plt.show()